﻿package edu.amm.neur;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;

import ru.vsu.amm.algebra.Matrix;

/**
 * Перцептрон - однослойная нейронная сеть с бинарными входами и выходами.
 * 
 * @author Илья
 * 
 */
public class Perceptron {
	
	private int inputCount;
	private int neuronCount;
	private Matrix weights;
	
	private List<Pair> pairs;
	private double eta = START_ETA;
	
	public Perceptron(int inputCount, int neuronCount) {
		if (inputCount < 1 || neuronCount < 1)
			throw new RuntimeException();
		
		this.inputCount = inputCount;
		this.neuronCount = neuronCount;
		
		weights = new Matrix(inputCount + 1, neuronCount);
		randomizeWeights();
	}
	
	public Perceptron(Matrix weights) {
		if (weights == null || weights.rows() < 2 || weights.columns() < 1)
			throw new RuntimeException();
		
		this.weights = weights;
		inputCount = weights.rows() - 1;
		neuronCount = weights.columns();
	}
	
	public int getInputCount() {
		return inputCount;
	}
	
	public int getNeuronCount() {
		return neuronCount;
	}
	
	public Matrix getWeights() {
		Matrix copy = new Matrix(weights.rows(), weights.columns());
		copy.copyFrom(weights);
		
		return copy;
	}
	
	/**
	 * Распознавание входного сигнала.
	 * @param input Вектор входного сигнала.
	 * @return Выходной вектор.
	 */
	public boolean[] recognize(boolean[] input) {
		if (input == null || input.length != inputCount)
			return null;
		
		boolean[] output = new boolean[neuronCount];
		for (int j = 0; j < output.length; j++)
			output[j] = output(j, input);
		
		return output;
	}
	
	/**
	 * Добавляет обучающую пару. Если перцептрон её не распознаёт,
	 * запускает процесс обучения.
	 * @param input Входной вектор обучающей пары.
	 * @param output Выходной вектор обучающей прары.
	 * @param <code>true</code>, если произошёл процесс обучения.
	 */
	public boolean addTrainigPair(boolean[] input, boolean[] output) {
		if (input == null || input.length != inputCount || output == null || output.length != neuronCount)
			throw new RuntimeException();
		
		if (pairs == null)
			pairs = new ArrayList<>();
		
		boolean[] Y = recognize(input);
		if (Arrays.equals(Y, output)) {
			if (findTrainingWithInput(input) == null)
				pairs.add(new Pair(input, output));
			
			return false;
		}
		
		if (findTrainingWithInput(input) != null)
			throw new RuntimeException();
		
		Pair pair = new Pair(input, output); 
		pairs.add(pair);
		
		train(pair, Y);
		
		return true;
	}
	
	/**
	 * @param typeIndex Номер типа изображения.
	 * @return Выходной вектор, соответствующий данному типу изображения.
	 */
	public boolean[] getVectorForImageType(int typeIndex) {
		if (typeIndex < 0)
			throw new RuntimeException();
		
		if (neuronCount == 1) {
			if (typeIndex >= 2)
				throw new RuntimeException();
			
			return new boolean[] { typeIndex == 1 };
		} else {
			if (typeIndex >= neuronCount)
				throw new RuntimeException();
			
			boolean[] vector = new boolean[neuronCount];
			vector[typeIndex] = true;
			
			return vector;
		}
	}
	
	/**
	 * @param vector Вектор выхода перцептрона.
	 * @return Индекс типа изображения, соответствующего выходу перцептрона,
	 * либо <code>-1</code>, если данному вектору не соответтсвует ни один из
	 * типов изображений, распознаваемых перцептроном.
	 */
	public int getTypeIndexForVector(boolean[] vector) {
		int imageTypesCount = neuronCount == 1 ? 2 : neuronCount;
		for (int i = 0; i < imageTypesCount; i++)
			if (Arrays.equals(getVectorForImageType(i), vector))
				return i;
		
		return -1;
	}
	
	private void randomizeWeights() {
		for (int i = 0; i < weights.rows(); i++)
			for (int j = 0; j < weights.columns(); j++)
				weights.set(LEFT + ((double) random.nextInt(Integer.MAX_VALUE) / DENOM) * LENGTH, i, j);
	}
	
	/**
	 * Выход <code>j</code>-го нейрона.
	 * @param j Номер нейрона.
	 * @param input Вход.
	 * @return Значение выхода.
	 */
	private boolean output(int j, boolean[] input) {
		double s = weights.get(0, j);
		for (int i = 0; i < input.length; i++)
			if (input[i])
				s += weights.get(i + 1, j);
		
		return s > 0 ? true : false; 
	}
	
	/**
	 * Запускает процесс обучения.
	 * 
	 * @param Пара, на которой перцептрон ошибается.
	 * @param Y Ошибочный выход перцептрона.
	 */
	private void train(Pair pair, boolean[] Y) {
		correct(pair.input, pair.output, Y);
		
		boolean corrected;
		int size = pairs.size();
		do {
			corrected = false;
			for (int i = 0; i < size; i++) {
				pair = pairs.get(i);
				Y = recognize(pair.input);
				if (Arrays.equals(Y, pair.output))
					continue;
				
				correct(pair.input, pair.output, Y);
				corrected = true;
			}
		} while (corrected);
	}
	
	private void correct(boolean[] Xk, boolean[] Yk, boolean[] Y) {
		for (int j = 0; j < weights.columns(); j++) {
			double delta = asDouble(Yk[j]) - asDouble(Y[j]);
			for (int i = 0; i < weights.rows(); i++) {
				if (i > 0 && !Xk[i - 1])
					continue;
				
				weights.set(weights.get(i, j) + eta * delta, i, j);
			}
		}
		
		eta *= CORRECT_FACTOR;
	}
	
	private Pair findTrainingWithInput(boolean[] input) {
		if (pairs == null)
			return null;
		
		for (Pair pair : pairs)
			if (Arrays.equals(pair.input, input))
				return pair;
		
		return null;
	}
	
	private static Random random = new Random();
	
	private static final double LEFT = -1.0;
	private static final double RIGHT = 1.0;
	private static final double LENGTH = RIGHT - LEFT;
	private static final int DENOM = Integer.MAX_VALUE - 1;
	
	private static final double START_ETA = 1.0 / 2;
	private static final double CORRECT_FACTOR = 0.99;
	
	private static class Pair {
		private boolean[] input;
		private boolean[] output;
		
		public Pair(boolean[] input, boolean[] output) {
			this.input = input;
			this.output = output;
		}
	}
	
	private static double asDouble(boolean bool) {
		return bool ? 1.0 : 0.0;
	}
}